package pl.edu.agh.neuraleconomy.core.simulation;

import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import lombok.Getter;
import lombok.ToString;

import org.apache.log4j.Logger;

import pl.edu.agh.neuraleconomy.common.utils.DateUtils;
import pl.edu.agh.neuraleconomy.core.exception.CoreException;
import pl.edu.agh.neuraleconomy.model.exchange.Company;
import pl.edu.agh.neuraleconomy.model.simulation.Simulation;

@ToString(callSuper = true, exclude = { "service", "logger", "history", "sharesMap", "transactions", "shortSelledSharesMap" })
public class SimulationEngine extends Simulation {
	private Logger logger = Logger.getLogger(getClass());
	private SimulationService service = new SimulationService();

	@Getter
	private List<DailyHistory> history = new LinkedList<DailyHistory>();
	@Getter
	private Map<Company, Share> sharesMap = new HashMap<Company, Share>();
	@Getter
	private Map<Company, Share> shortSelledSharesMap = new HashMap<Company, Share>();
	@Getter
	private List<Transaction> transactions = new LinkedList<Transaction>();

	public SimulationEngine(Date startDate, Double startMoney, String name) {
		this.startDate = DateUtils.trimDate(startDate);
		this.currentDate = DateUtils.trimDate(startDate);
		this.startMoney = new BigDecimal(startMoney).setScale(2, BigDecimal.ROUND_HALF_UP);
		this.currentMoney = new BigDecimal(startMoney).setScale(2, BigDecimal.ROUND_HALF_UP);
		this.name = name;
	}

	public SimulationEngine(Simulation simulation) {
		this.setId(simulation.getId());
		this.startDate = simulation.getStartDate();
		this.currentDate = simulation.getCurrentDate();
		this.startMoney = simulation.getStartMoney().setScale(2, BigDecimal.ROUND_HALF_UP);
		this.currentMoney = simulation.getCurrentMoney().setScale(2, BigDecimal.ROUND_HALF_UP);
		this.name = simulation.getName();
	}

	public void nextDay() throws CoreException {
		if (DateUtils.trimDate(new Date()).equals(currentDate)) {
			throw new CoreException("Simulation date cannot be in future");
		}

		DailyHistory dailyHistory = new DailyHistory(currentDate, currentMoney, this);
		history.add(dailyHistory);

		currentDate = DateUtils.addDays(currentDate, 1);
		logger.debug("Next day\n" + this.toString());
	}

	public BigDecimal getWealth() {
		BigDecimal wealth = BigDecimal.ZERO;

		for (Company c : sharesMap.keySet()) {
			wealth = wealth.add(service.getSharePrice(c, currentDate).multiply(new BigDecimal(getSharesAmmount(c))));
		}

		wealth = wealth.add(getCurrentPureMoney());

		return wealth;
	}

	public int getSharesAmmount(Company company) {
		return sharesMap.get(company) == null ? 0 : sharesMap.get(company).getAmount();
	}

	public int getShortSelledSharesAmmount(Company company) {
		return shortSelledSharesMap.get(company) == null ? 0 : shortSelledSharesMap.get(company).getAmount();
	}

	public BigDecimal getCurrentPureMoney() {
		return currentMoney.subtract(getShortSelledWealth());
	}

	public BigDecimal getShortSelledWealth() {
		BigDecimal wealth = BigDecimal.ZERO;

		for (Share s : shortSelledSharesMap.values()) {
			wealth = wealth.add(service.getSharePrice(s.getCompany(), currentDate).multiply(BigDecimal.valueOf(s.getAmount())));
		}

		return wealth;
	}

	public void makeTransaction(Transaction transaction) throws CoreException {
		if (transaction == null) {
			return;
		}
		logger.debug("Trying to make transaction\n" + transaction.toString());

		switch (transaction.getType()) {
		case BUY:
			makeBuy(transaction);
			break;
		case SELL:
			makeSell(transaction);
			break;
		case SELL_SHORT:
			makeShortSell(transaction);
			break;
		default:
			logger.warn("Non supported transaction type: " + transaction.getType());
			return;
		}

		// transaction.setPrice(service.getSharePrice(transaction.getCompany(),
		// transaction.getDate()));
		transactions.add(transaction);
		logger.debug("State after transaction\n" + this.toString());
	}

	private void makeBuy(Transaction transaction) throws CoreException {
		validateBuyTransaction(transaction);

		// pay for shares
		BigDecimal transactionPrice = getTransactionPrice(transaction);
		currentMoney = currentMoney.subtract(transactionPrice);

		int sharesLeft = transaction.getAmount();

		// try to buy short selled shares
		Share shortSelledShare = shortSelledSharesMap.get(transaction.getCompany());
		if (shortSelledShare != null) {
			int bought = Math.min(transaction.getAmount(), shortSelledShare.getAmount());

			shortSelledShare.removeShares(bought);
			sharesLeft -= bought;
		}

		if (sharesLeft > 0) {
			// receive shares
			Share share = sharesMap.get(transaction.getCompany());
			if (share == null) {
				share = new Share(transaction.getCompany(), sharesLeft, this);
				sharesMap.put(transaction.getCompany(), share);
			} else {
				share.addShares(sharesLeft);
			}
		}
	}

	private void makeSell(Transaction transaction) throws CoreException {
		validateSellTransaction(transaction);

		// sell shares
		Share share = sharesMap.get(transaction.getCompany());
		share.removeShares(transaction.getAmount());

		// receive money :)
		BigDecimal transactionPrice = getTransactionPrice(transaction);
		currentMoney = currentMoney.add(transactionPrice);
	}

	private void makeShortSell(Transaction transaction) {
		validateShortSellTransaction(transaction);

		Share share = shortSelledSharesMap.get(transaction.getCompany());
		if (share == null) {
			share = new Share(transaction.getCompany(), transaction.getAmount(), this);
			shortSelledSharesMap.put(transaction.getCompany(), share);
		} else {
			share.addShares(transaction.getAmount());
		}

		BigDecimal transactionPrice = getTransactionPrice(transaction);
		currentMoney = currentMoney.add(transactionPrice);
	}

	private void validateBuyTransaction(Transaction transaction) throws CoreException {
		BigDecimal transactionPrice = getTransactionPrice(transaction);
		if (transactionPrice.compareTo(currentMoney) > 0) {
			throw new CoreException("Cannot make buy transaction. Transaction price is: " + String.valueOf(transactionPrice)
					+ ", but you have only: " + String.valueOf(currentMoney));
		}
	}

	private void validateSellTransaction(Transaction transaction) throws CoreException {
		int sharesAmmount = getSharesAmmount(transaction.getCompany());
		if (sharesAmmount < transaction.getAmount()) {
			throw new CoreException("Cannot make sell transaction. You are trying to sell: " + transaction.getAmount() + ", while having "
					+ sharesAmmount + " shares");
		}
	}

	private void validateShortSellTransaction(Transaction transaction) {

	}

	private BigDecimal getTransactionPrice(Transaction transaction) {
		return service.getSharePrice(transaction.getCompany(), currentDate).multiply(new BigDecimal(transaction.getAmount()));
	}
}
